package javango.contrib.hibernate;

import com.google.inject.Inject;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map.Entry;

import javango.contrib.hibernate.HibernateFormHelper;
import javango.contrib.hibernate.HibernateUtil;
import javango.contrib.hibernate.ModelDao;
import javango.forms.fields.Field;
import javango.forms.fields.FieldFactory;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.hibernate.MappingException;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.SessionFactoryImplementor;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;


public class ModelForm extends javango.forms.ModelForm {
	
	private final static Log log = LogFactory.getLog(ModelForm.class);

	protected HibernateUtil hibernateUtil;

	public ModelForm() {
		this(
            JavangoModule.injector.getInstance(FieldFactory.class), 
			JavangoModule.injector.getInstance(HibernateUtil.class)
		);		
    }
    
	public ModelForm(Class clazz) {
		this();
		setModel(clazz);
	}
	
	public ModelForm(Object bean) {
		this();
		setInitial(bean);
	} 
    
    @Inject
	public ModelForm(FieldFactory fieldFactory, HibernateUtil hibernateUtil) {
		super(fieldFactory);
		this.hibernateUtil = hibernateUtil;
	}
		
	@Override
	public ModelForm setInitial(Object bean) {
		if (this.model == null) {
			this.setModel(bean.getClass());
		}

		init();
				
		try {
			ModelDao dao = new ModelDao(hibernateUtil, model);

			Configuration cfg = hibernateUtil.getConfiguration();		
			PersistentClass pc = cfg.getClassMapping(model.getName());
						
			for(Entry<String, Field> e: _fields.entrySet()) {
				try {
					Property property = pc.getProperty(e.getKey());
					if (property.getType().isAssociationType()) {
						log.debug(e.getKey() + " is an association: ");
						Object o = PropertyUtils.getProperty(bean, e.getKey());
						if (o != null) {
							if (o instanceof Collection<?>) {
								Collection<?> c = (Collection<?>) o;
								Object[] initial = new Object[c.size()];
								int ctr=0;
								for (Object obj : c) {
									initial[ctr++] = dao.getPk(obj);
								}
								e.getValue().setInitial(initial);
							} else {
								e.getValue().setInitial(dao.getPk(o));
							}
						}
					} else {
						e.getValue().setInitial(PropertyUtils.getProperty(bean, e.getKey()));
					}
				} catch (MappingException ex) {
					if (log.isDebugEnabled()) log.debug("SKipping unmapped field " + e.getKey());
				}				
			}

		} catch (Exception e) {
			log.error(e,e);
				// TODO What should be done here?
		}
		return this;
	}
	
	@Override
	public boolean isValid() {
		if (!super.isValid()) return false;
		
		// check the validators defined in the form
		if (!HibernateFormHelper.isValid(this)) return false;

		// check the validators defined in the modelClass
		return HibernateFormHelper.isValid(this, getModel());
	}
	
	protected Field getFieldType(Property property, java.lang.reflect.Field field) {
		if (property.getType() instanceof CollectionType) {
			// TODO Need to figure out correct way to may collections / multi way collections.
			return null;
//			AssociationType assType = (AssociationType)property.getType();
//			String c = assType.getAssociatedEntityName((SessionFactoryImplementor)HibernateUtil.getSessionFactory());
//			try {
//				return new ModelMultipleChoiceField(Class.forName(c)).setRequired(false); // TODO how to better control this..  which come first the chicken or the egg?
//			} catch (ClassNotFoundException e) {
//				log.error(e,e);
//				return null;
//			}
		} else if (property.getType() instanceof AssociationType) {
			AssociationType assType = (AssociationType)property.getType();
			String c = assType.getAssociatedEntityName((SessionFactoryImplementor)hibernateUtil.getSessionFactory());
			try {
				return new ModelChoiceField(fieldFactory.getWidgetFactory(), hibernateUtil, Class.forName(c));
			} catch (ClassNotFoundException e) {
				log.error(e,e);
				return null;
			}
		} else {
			return fieldFactory.newFieldForClass(field.getType());
		}

	}
	
	protected java.lang.reflect.Field findField(Class modelClass, Property property) throws NoSuchFieldException {
		Class classToTry = modelClass;
		while(classToTry != null) {
			try {
				return classToTry.getDeclaredField(property.getName());
			} catch (NoSuchFieldException e) {
				classToTry = classToTry.getSuperclass();
			}
		}
		return modelClass.getDeclaredField(property.getName()); // let this throw the excpetion,  i guess we could do so ourselves...
	}
	
	protected void processField(Property property, String fieldName) {
		if (log.isDebugEnabled()) log.debug("Processing Field :" + fieldName);
		try {
			// first see if this modelform has overridden the field...
			java.lang.reflect.Field field = this.getClass().getDeclaredField(fieldName);
			loadField(field);

			// TODO Process hibernate specific annotations (ie column length) - wehat order should this be done in??  what should be overridding?

		} catch (NoSuchFieldException e) {
			try {
				java.lang.reflect.Field field = findField(model, property);
				javango.forms.fields.Field baseField = null;
				baseField = getFieldType(property, field);
				
				if (baseField != null) {
					baseField.setName(property.getName());
					Annotation[] annotations = field.getAnnotations();
					// TODO Process hibernate specific annotations (ie column length)
					for (Annotation annotation: annotations) {
							baseField.handleAnnotation(annotation);
					}
					_fields.put(field.getName(), baseField);
				}
			} catch (NoSuchFieldException e1) {
				log.debug("Unable to find field");
			}
		}

	}
	
	@Override
	protected void processModel() {		
		Configuration cfg = hibernateUtil.getConfiguration();		
		PersistentClass pc = cfg.getClassMapping(model.getName());

		if (pc == null) {
			// could not find hibernate configuration for this form,  fallback
			super.processModel();
			return;
		}
		
		if (include != null) {
			// only process the fields specified in include,  preserving order
			for(String fieldName : include) {
				try {
					processField(pc.getProperty(fieldName), fieldName);
				} catch (MappingException e) {
					log.error("Field not found :" + fieldName);
				}
			}
		} else {
			Iterator<Property> propIterator = pc.getPropertyIterator();
			while (propIterator.hasNext()) {
				Property property = propIterator.next();
				String fieldName = property.getName();
				if (exclude != null && exclude.contains(fieldName)) {
					if (log.isDebugEnabled()) log.debug("Excluding Field :" + fieldName);
				} else {
					processField(property, fieldName);
				}
			}
		}
	}
	
}
